use std::convert::Infallible;
use std::error::Error as StdError;
use std::fmt::{self, Display};
use std::io;
use std::result;

/// Errors than can occur while parsing the response from the server.
#[derive(Debug)]
pub enum InvalidResponseKind {
    /// Invalid or missing Location header in redirection
    LocationHeader,
    /// Invalid redirection URL
    RedirectionUrl,
    /// Status line
    StatusLine,
    /// Status code
    StatusCode,
    /// Error parsing header
    Header,
    /// Error decoding chunk size
    ChunkSize,
    /// Error decoding chunk
    Chunk,
    /// Invalid Content-Length header
    ContentLength,
}

impl Display for InvalidResponseKind {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        use InvalidResponseKind::*;

        match self {
            LocationHeader => write!(f, "missing or invalid location header"),
            RedirectionUrl => write!(f, "invalid redirection url"),
            StatusLine => write!(f, "invalid status line"),
            StatusCode => write!(f, "invalid status code"),
            Header => write!(f, "invalid header"),
            ChunkSize => write!(f, "invalid chunk size"),
            Chunk => write!(f, "invalid chunk"),
            ContentLength => write!(f, "invalid content length"),
        }
    }
}

/// Common errors that can occur during HTTP requests.
#[derive(Debug)]
pub enum ErrorKind {
    /// CONNECT is not supported.
    ConnectNotSupported,
    /// Could not connect to proxy with CONNECT method.
    ConnectError {
        /// Status code from the proxy.
        status_code: http::StatusCode,
        /// Up to 10 KiB of body data from the proxy which might help diagnose the error.
        body: Vec<u8>,
    },
    /// Error generated by the `http` crate.
    Http(http::Error),
    /// IO Error
    Io(io::Error),
    /// Invalid base URL given to the Request.
    InvalidBaseUrl,
    /// An URL with an invalid host was found while processing the request.
    InvalidUrlHost,
    /// The URL scheme is unknown and the port is missing.
    InvalidUrlPort,
    /// Server sent an invalid response.
    InvalidResponse(InvalidResponseKind),
    /// Too many redirections
    TooManyRedirections,
    /// Status code indicates failure
    StatusCode(http::StatusCode),
    /// JSON decoding/encoding error.
    #[cfg(feature = "json")]
    Json(serde_json::Error),
    /// Form-URL encoding error.
    #[cfg(feature = "form")]
    UrlEncoded(serde_urlencoded::ser::Error),
    /// TLS error encountered while connecting to an https server.
    #[cfg(feature = "tls")]
    Tls(native_tls::Error),
    /// Invalid DNS name used for TLS certificate verification
    #[cfg(feature = "tls-rustls")]
    InvalidDNSName(webpki::InvalidDNSNameError),
    /// Invalid mime type in a Multipart form
    InvalidMimeType(String),
    /// TLS was not enabled by features.
    TlsDisabled,
    /// WebPKI error.
    #[cfg(feature = "tls-rustls")]
    WebPKI(webpki::Error),
}

/// A type that contains all the errors that can possibly occur while accessing an HTTP server.
#[derive(Debug)]
pub struct Error(pub(crate) Box<ErrorKind>);

impl Error {
    /// Get a reference to the `ErrorKind` inside.
    pub fn kind(&self) -> &ErrorKind {
        &self.0
    }

    /// Comsume this `Error` and get the `ErrorKind` inside.
    pub fn into_kind(self) -> ErrorKind {
        *self.0
    }
}

impl Display for Error {
    fn fmt(&self, w: &mut fmt::Formatter) -> fmt::Result {
        use ErrorKind::*;

        match *self.0 {
            ConnectNotSupported => write!(w, "CONNECT is not supported"),
            ConnectError { status_code, .. } => write!(w, "Proxy CONNECT error: {}", status_code),
            Http(ref e) => write!(w, "Http Error: {}", e),
            Io(ref e) => write!(w, "Io Error: {}", e),
            InvalidBaseUrl => write!(w, "Invalid base URL"),
            InvalidUrlHost => write!(w, "URL is missing a host"),
            InvalidUrlPort => write!(w, "URL is missing a port"),
            InvalidResponse(ref k) => write!(w, "InvalidResponse: {}", k),
            TooManyRedirections => write!(w, "Too many redirections"),
            StatusCode(ref sc) => write!(w, "Status code {} indicates failure", sc),
            #[cfg(feature = "json")]
            Json(ref e) => write!(w, "Json Error: {}", e),
            #[cfg(feature = "form")]
            UrlEncoded(ref e) => write!(w, "URL Encoding Error: {}", e),
            #[cfg(feature = "tls")]
            Tls(ref e) => write!(w, "Tls Error: {}", e),
            #[cfg(feature = "tls-rustls")]
            InvalidDNSName(ref e) => write!(w, "Invalid DNS name: {}", e),
            InvalidMimeType(ref e) => write!(w, "Invalid mime type: {}", e),
            TlsDisabled => write!(w, "TLS is disabled, activate tls or tls-rustls feature"),
            #[cfg(feature = "tls-rustls")]
            WebPKI(ref e) => write!(w, "WebPKI error: {}", e),
        }
    }
}

impl StdError for Error {
    fn cause(&self) -> Option<&dyn StdError> {
        use ErrorKind::*;

        match *self.0 {
            Io(ref e) => Some(e),
            Http(ref e) => Some(e),
            #[cfg(feature = "json")]
            Json(ref e) => Some(e),
            #[cfg(feature = "tls")]
            Tls(ref e) => Some(e),
            #[cfg(feature = "tls-rustls")]
            InvalidDNSName(ref e) => Some(e),
            _ => None,
        }
    }
}

impl From<Infallible> for Error {
    fn from(_err: Infallible) -> Error {
        unreachable!()
    }
}

impl From<io::Error> for Error {
    fn from(err: io::Error) -> Error {
        Error(Box::new(ErrorKind::Io(err)))
    }
}

impl From<http::Error> for Error {
    fn from(err: http::Error) -> Error {
        Error(Box::new(ErrorKind::Http(err)))
    }
}

impl From<http::header::InvalidHeaderValue> for Error {
    fn from(err: http::header::InvalidHeaderValue) -> Error {
        Error(Box::new(ErrorKind::Http(http::Error::from(err))))
    }
}

#[cfg(feature = "tls")]
impl From<native_tls::Error> for Error {
    fn from(err: native_tls::Error) -> Error {
        Error(Box::new(ErrorKind::Tls(err)))
    }
}

#[cfg(feature = "tls-rustls")]
impl From<webpki::InvalidDNSNameError> for Error {
    fn from(err: webpki::InvalidDNSNameError) -> Error {
        Error(Box::new(ErrorKind::InvalidDNSName(err)))
    }
}

#[cfg(feature = "json")]
impl From<serde_json::Error> for Error {
    fn from(err: serde_json::Error) -> Error {
        Error(Box::new(ErrorKind::Json(err)))
    }
}

#[cfg(feature = "form")]
impl From<serde_urlencoded::ser::Error> for Error {
    fn from(err: serde_urlencoded::ser::Error) -> Error {
        Error(Box::new(ErrorKind::UrlEncoded(err)))
    }
}

impl From<ErrorKind> for Error {
    fn from(err: ErrorKind) -> Error {
        Error(Box::new(err))
    }
}

impl From<InvalidResponseKind> for Error {
    fn from(kind: InvalidResponseKind) -> Error {
        ErrorKind::InvalidResponse(kind).into()
    }
}

impl From<Error> for io::Error {
    fn from(err: Error) -> io::Error {
        io::Error::new(io::ErrorKind::Other, err)
    }
}

impl From<InvalidResponseKind> for io::Error {
    fn from(kind: InvalidResponseKind) -> io::Error {
        io::Error::new(io::ErrorKind::Other, Error(Box::new(ErrorKind::InvalidResponse(kind))))
    }
}

#[cfg(feature = "tls-rustls")]
impl From<webpki::Error> for Error {
    fn from(err: webpki::Error) -> Error {
        Error(Box::new(ErrorKind::WebPKI(err)))
    }
}

/// Wrapper for the `Result` type with an `Error`.
pub type Result<T = ()> = result::Result<T, Error>;
